package tw.com.hitevision.whiteboard.android.util;

import android.text.Layout;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.util.Log;
import android.widget.TextView;

import org.jetbrains.annotations.NotNull;

public class FontSizeCalculator {
    private static final String TAG = FontSizeCalculator.class.getSimpleName();
    private static final int INVALID_VALUE = -1;
    private static final int MAX_RETRY = 10;
    private final TextView textBox;
    private TextPaint tempTextPaint;
    private float adjustedRate;
    private float adjustedFontSize;

    public FontSizeCalculator(TextView textView) {
        textBox = textView;
        tempTextPaint = new TextPaint(textBox.getPaint());
        init(textBox.getTextSize());
    }

    private void init(float size) {
        adjustedFontSize = size;
        adjustedRate = 1;
        tempTextPaint.reset();
    }

    public float getAdjustedRate() {
        return adjustedRate;
    }

    public float getAdjustedFontSize() {
        return adjustedFontSize;
    }

    public void calculateFontSizeAndRateToFitTextView(float rate, float minSize, float maxSize) {
        if (textBox == null || textBox.getText().length() <= 0 || rate == 1 || minSize >= maxSize || minSize <= 0) {
            return;
        }

        init(textBox.getTextSize());

        float result = 1;
        int retry = MAX_RETRY;
        float r = rate;

        textBox.measure(0, 0);
        float widthOffset = textBox.getMeasuredWidth() * Math.abs(1 - rate);
        float offset = Math.min(1 / widthOffset, 0.005f);

        while (retry > 0) {
            result = getFontSizeFitScaledTextView(r);
            if (result > 0) {
                break;
            }
            r += offset;
            retry--;
        }

        if (result == INVALID_VALUE || result < minSize) {
            result = minSize;
            r = getTextScalingRateFromFont(minSize);
        } else if (result > maxSize) {
            result = maxSize;
            r = getTextScalingRateFromFont(maxSize);
        }

        adjustedFontSize = result;
        adjustedRate = r;
    }

    private float getFontSizeFitScaledTextView(float rate) {
        final float oriFontSize = textBox.getTextSize();
        final float threshold = 0.000001f;
        final String text = textBox.getText().toString();
        final int targetLines = textBox.getLineCount();

        tempTextPaint.set(textBox.getPaint());
        textBox.measure(0, 0);

        final float availableW = textBox.getMeasuredWidth() * rate - textBox.getTotalPaddingLeft() - textBox.getTotalPaddingRight();
        final int oriW = textBox.getMeasuredWidth() - textBox.getTotalPaddingLeft() - textBox.getTotalPaddingRight();
        final int oriLastBreakId = getBreakIndexAtLastLine(oriW, tempTextPaint, text, targetLines);

        if (tempTextPaint.measureText(text) <= 0 || targetLines <= 0 || availableW <= 0) {
            return 0;
        }

        float approximateSize = oriFontSize * rate;

        tempTextPaint.setTextSize(approximateSize);

        final int resultLines = getLineCountWithScaledW(Math.round(availableW), tempTextPaint);
        final int lastBreakId = getBreakIndexAtLastLine(Math.round(availableW), tempTextPaint, text, targetLines);

        float p, q;
        if (resultLines == targetLines && lastBreakId == oriLastBreakId) {
            return approximateSize;
        } else {
            p = approximateSize;
            if (resultLines != targetLines) {
                q = (resultLines > targetLines) ? (approximateSize * 0.5f) : (approximateSize * 2.0f);
            } else {
                q = (lastBreakId > oriLastBreakId) ? (approximateSize * 2.0f) : (approximateSize * 0.5f);
            }

            int loopTimes = 1;
            boolean achieve = false;
            boolean breakAchieve = false;
            final int targetLinesOffset = resultLines - targetLines;

            while (Math.abs(p - q) > threshold && loopTimes <= 30) {
                float resultSize = (p + q) / 2;

                tempTextPaint.setTextSize(resultSize);
                int lines = getLineCountWithScaledW(Math.round(availableW), tempTextPaint);
                if (lines != targetLines) {
                    if (achieve) {
                        break;
                    }

                    if (resultSize == p || resultSize == q) {
                        return INVALID_VALUE;
                    }

                    if ((targetLinesOffset * (lines - targetLines)) > 0) { // (lines - targetLines) : 1 -> 1 -> -1 ==> cross the correct font size range!
                        p = resultSize;
                    } else {
                        q = resultSize;
                    }

                } else {
                    achieve = true;
                    if (getBreakIndexAtLastLine(Math.round(availableW), tempTextPaint, text, targetLines) != oriLastBreakId) {
                        if (breakAchieve) {
                            break;
                        }
                    } else {
                        breakAchieve = true;
                    }
                    q = resultSize;
                }
//                q = resultSize;
                loopTimes++;
            }

//            return q;
            tempTextPaint.setTextSize(q);
            boolean isLineCountRight = getLineCountWithScaledW(Math.round(availableW), tempTextPaint) == targetLines;
            boolean isBreakIdRight = getBreakIndexAtLastLine(Math.round(availableW), tempTextPaint, text, targetLines) == oriLastBreakId;

            return (isLineCountRight && isBreakIdRight) ? q : INVALID_VALUE;
        }

    }

    private float getTextScalingRateFromFont(float newFontSize) {
        final float oriFontSize = textBox.getTextSize();
        final String text = textBox.getText().toString();
        final int threshold = 1;
        final int totalPadding = textBox.getTotalPaddingLeft() + textBox.getTotalPaddingRight();
        final int targetLines = textBox.getLineCount();

        tempTextPaint.set(textBox.getPaint());
        textBox.measure(0, 0);

        if (tempTextPaint.measureText(text) <= 0 || newFontSize == oriFontSize) {
            return 1;
        }

        final int oriW = textBox.getMeasuredWidth() - totalPadding;
        final int oriLastBreakId = getBreakIndexAtLastLine(oriW, tempTextPaint, text, targetLines);

        float oriTextLength = tempTextPaint.measureText(text);
        tempTextPaint.setTextSize(newFontSize);
        float approximateRate = tempTextPaint.measureText(text) / oriTextLength;
//        return approximateRate;

        final float scaledW = oriW * approximateRate;

        final int resultLines = getLineCountWithScaledW(Math.round(scaledW), tempTextPaint);
        final int lastBreakId = getBreakIndexAtLastLine(Math.round(scaledW), tempTextPaint, text, targetLines);

        if (resultLines == targetLines && oriLastBreakId == lastBreakId) {
            return (totalPadding + scaledW) / (totalPadding + oriW);
        } else {
            float p, q;
            p = scaledW;
            if (resultLines != targetLines) {
                q = (resultLines < targetLines) ? Math.max(scaledW * 0.5f, 1) : (scaledW * 2.0f);
            } else {
                q = (lastBreakId > oriLastBreakId) ? Math.max(scaledW * 0.5f, 1) : (scaledW * 2.0f);
            }

            int loopTimes = 1;
            boolean achieve = false;
            boolean breakAchieve = false;
            final int targetLinesOffset = resultLines - targetLines;

            while (Math.abs(p - q) > threshold && loopTimes <= 20) {
                float resultW = (p + q) / 2;

                int lines = getLineCountWithScaledW(Math.round(resultW), tempTextPaint);
                if (lines != targetLines) {
                    if (achieve) {
                        break;
                    }

                    if (resultW == p || resultW == q) {
                        break;
                    }

                    if ((targetLinesOffset * (lines - targetLines)) > 0) { // (lines - targetLines) : 1 -> 1 -> -1 ==> cross the correct font size range!
                        p = resultW;
                    } else {
                        q = resultW;
                    }

                } else {
                    achieve = true;
                    if (getBreakIndexAtLastLine(Math.round(resultW), tempTextPaint, text, targetLines) != oriLastBreakId) {
                        if (breakAchieve) {
                            break;
                        }
                    } else {
                        breakAchieve = true;
                    }
                    q = resultW;
                }
                loopTimes++;
            }

            return (totalPadding + q) / (totalPadding + oriW);

        }
    }

    private int getLineCountWithScaledW(int resultW, TextPaint textPaint) {
        StaticLayout layout = new StaticLayout(textBox.getText(), textPaint, resultW, Layout.Alignment.ALIGN_NORMAL,
                textBox.getLineSpacingMultiplier(), textBox.getLineSpacingExtra(), false);

        return layout.getLineCount();
    }

    // It works well when the original textView and the scaled textView have the same line count.
    private int getBreakIndexAtLastLine(int resultW, TextPaint textPaint, @NotNull String text, int targetLine) {
        StaticLayout layout = new StaticLayout(text, textPaint, resultW, Layout.Alignment.ALIGN_NORMAL,
                textBox.getLineSpacingMultiplier(), textBox.getLineSpacingExtra(), false);
        int lastLineId = targetLine - 1;
        int id = (targetLine <= layout.getLineCount() ? -1 : Integer.MAX_VALUE);
//        int id = -1;
        for (int i = text.length() - 1; i >= 0; i--) {
            if (lastLineId != layout.getLineForOffset(i)) {
                break;
            }
            id = i;
        }
        return id;
    }

    private void log(String str) {
        Log.d(TAG, "(Yuda)" + str);
    }

}
